-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
--
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local utils = require 'mc.utils'
local log = require 'mc.logging'
local cjson = require 'cjson'
local mdb_service = require 'mc.mdb.mdb_service'

local DISK_ARRAY_INTERFACE<const> = 'bmc.kepler.Systems.Storage.DiskArray'
local DRIVE_INTERFACE<const> = 'bmc.kepler.Systems.Storage.Drive'
local BROADCOM_RAID_CHECK_FLAG<const> = 0x8000
local EXPECT_PATH_END_OFFSET<const> = 2
--1TB = 1048576(1024*1024) MB; 1GB = 1024 MB
local TB_TO_MB_FACTOR<const> = 1048576
local GB_TO_MB_FACTOR<const> = 1024
--最大可能的逻辑盘容量值
local SML_LD_MAX_CAPACITY_IN_MB<const> = 0xFFFFFFFE
local SML_MAX_HOLES_IN_ARRAY<const> = 32
local SNMP_LD_NAME_LENGTH<const> = 64
local SML_LD_CAPACITY_UNIT_MB<const> = 0
local SML_LD_CAPACITY_UNIT_GB<const> = 1
local SML_LD_CAPACITY_UNIT_TB<const> = 2

local m = {}

--逻辑盘相关处理接口
local function get_disk_array_path(id)
    local filter = cjson.encode({Id = id})
    local ok, rsp = pcall(mdb_service.get_path, bus, DISK_ARRAY_INTERFACE, filter, false)
    if not ok then
        log:error("Get disk array path failed, ret: %s", rsp)
        return nil
    end
    return rsp.Path
end

local function get_disk_array_obj(id)
    if id == 0xffff then
        return nil
    end

    local path = get_disk_array_path(id)
    if path == nil then
        return nil
    end

    local ok, obj = pcall(function ()
        return mdb.get_object(bus, path, DISK_ARRAY_INTERFACE)
    end)
    if not ok then
        log:error('Get disk array object failed, ret = %s', obj)
        return nil
    end
    return obj
end

--同一个volume下的span，必须配置相同个数的disk
--返回值{span个数， 每个span有几个Drive}
function m.get_spans_info(ref_disk_array_list)
    local array_list = {}
    local span_list = {}

    for index, disk_array_id in ipairs(ref_disk_array_list) do
        local obj = get_disk_array_obj(disk_array_id)
        if not obj then
            goto continue
        end
        array_list[#array_list + 1] = #obj.RefDrives

        if disk_array_id >= BROADCOM_RAID_CHECK_FLAG then
            span_list[#span_list + 1] = #obj.RefDrives
        end
        ::continue::
    end

    local targe_span = (#span_list == 0) and array_list or span_list
    if #targe_span == 0 then
        log:error('get_spans_info error: span num is empty')
        return {0, 0}
    end
    return {#targe_span, targe_span[1]}
end

--用于logicalDriveProperty logicalDriveDelete set接口
function m.get_ref_controller_interface_path(volume_path)
    local path_end_index = volume_path:find('Volumes', 1, true)
    return volume_path:sub(1, path_end_index - EXPECT_PATH_END_OFFSET)
end

--控制器相关处理接口
function m.get_para_drive_list(input_str)
    local match_str = input_str:match('pd=%s*([%d,%s]+)%s*;*%s*')
    if not match_str then
        error(base_messages.PropertyValueFormatError(input_str, 'pd'))
    end
    local drive_id_list = utils.split(match_str, ',')
    local ret_list = {}
    for i = 1, #drive_id_list do
        local num = tonumber(drive_id_list[i])
        if num then
            --snmp传过来的参数比内部drive Id多1
            ret_list[#ret_list + 1] = num - 1
        else
            error(base_messages.PropertyValueFormatError(drive_id_list[i], 'pd'))
        end
    end
    return ret_list
end

local raid_level_map = {
    ['1adm'] = 11,
    ['10adm'] = 12,
    ['1triple'] = 13,
    ['10triple'] = 14
}

-- 支持: r0,r1,r5,r6,r10,r50,r60,pmc额外支持r1adm,r10adm,r1triple,r10triple
function m.get_para_raid_type(input_str)
    local num_str, letters = input_str:match('rl=%s*r(%d+)([a-z]*)%s*;*%s*')
    if not num_str then
        error(base_messages.PropertyValueFormatError(input_str, 'rl'))
    end

    if raid_level_map[num_str..letters] then
        return raid_level_map[num_str..letters]
    end

    local raid_num = tonumber(num_str)
    if not raid_num then
        error(base_messages.PropertyValueFormatError(num_str, 'rl'))
        return 0xff
    end
    return raid_num
end


function m.get_para_span_depth(input_str)
    local num_str = input_str:match('sc=')
    if not num_str then
        return 0xff
    end

    num_str = input_str:match('sc=%s*(%d+)%s*;*%s*')
    if not num_str then
        error(base_messages.PropertyValueFormatError(input_str, 'sc'))
    end
    local span_num = tonumber(num_str)
    if not span_num then
        error(base_messages.PropertyValueFormatError(num_str, 'sc'))
    end
    return span_num
end

function m.get_para_name(input_str)
    local name_str = input_str:match('name=')
    if not name_str then
        return ''
    end
    name_str = input_str:match('name=%s*([ -~]-)%s*;')
    if not name_str then
        error(base_messages.PropertyValueFormatError(input_str, 'name'))
    end
    if #name_str > SNMP_LD_NAME_LENGTH then
        error(custom_messages.ValueOutOfRange('name'))
    end
    return name_str
end

local UNIT_MAP<const> = {
    ['m']= {SML_LD_CAPACITY_UNIT_MB, 1},
    ['M']= {SML_LD_CAPACITY_UNIT_MB, 1},
    ['g']= {SML_LD_CAPACITY_UNIT_GB, GB_TO_MB_FACTOR},
    ['G']= {SML_LD_CAPACITY_UNIT_GB, GB_TO_MB_FACTOR},
    ['t']= {SML_LD_CAPACITY_UNIT_TB, TB_TO_MB_FACTOR},
    ['T']= {SML_LD_CAPACITY_UNIT_TB, TB_TO_MB_FACTOR}
}

--返回一个元组，[1]:capacity, [2]:capacity_unit
function m.get_para_capacity_info(input_str)
    local size_str = input_str:match('size=')
    if not size_str then
        return {0xffffffff, 0xff}
    end
    size_str = input_str:match('size=%s*(%d+%.*%d*)%a%s*;*%s*')
    local uint_str = input_str:match('size=%s*%d+%.*%d*(%a)%s*;*%s*')
    if not size_str or not uint_str then
        error(base_messages.PropertyValueFormatError(input_str, 'size'))
    end

    local size_num = tonumber(size_str)
    if not size_num then
        error(base_messages.PropertyValueFormatError(input_str, 'size'))
    end

    if not UNIT_MAP[uint_str] then
        error(custom_messages.ValueOutOfRange('size'))
    end

    size_num = size_num * UNIT_MAP[uint_str][2]
    if size_num >= SML_LD_MAX_CAPACITY_IN_MB then
        size_num = SML_LD_MAX_CAPACITY_IN_MB
    else
        size_num = math.floor(size_num)
    end
    --业务映射表：m/M-0,g/G-1,t/T-2
    uint_code = 0
    return {size_num, uint_code}
end

local VALID_SIZE_LIST<const> = {
    ['16k'] = 5,
    ['32k'] = 6,
    ['64k'] = 7,
    ['128K'] = 8,
    ['256k'] = 9,
    ['512k'] = 10,
    ['1M'] = 11,
    ['16K'] = 5,
    ['32K'] = 6,
    ['64K'] = 7,
    ['128K'] = 8,
    ['256K'] = 9,
    ['512K'] = 10,
    ['1m'] = 11
}

function m.get_para_strip_size(input_str)
    local match_str = input_str:match('ss=')
    if not match_str then
        return 0xff
    end
    match_str = input_str:match('ss=%s*(%d+%a)%s*;*%s*')
    if not match_str or not VALID_SIZE_LIST[match_str] then
        error(base_messages.PropertyValueFormatError(input_str, 'ss'))
    end

    return VALID_SIZE_LIST[match_str]
end

function m.get_para_read_policy(input_str)
    local match_str = input_str:match('rp=')
    if not match_str then
        return 0xff
    end
    match_str = input_str:match('rp=%s*(%a+)%s*;*%s*')
    local valid_policy = {
        ['nra'] = 0,
        ['ra'] = 1
    }
    if not match_str or not valid_policy[match_str] then
        error(base_messages.PropertyValueFormatError(input_str, 'rp'))
    end

    return valid_policy[match_str]
end

function m.get_para_write_policy(input_str)
    local match_str = input_str:match('wp=')
    if not match_str then
        return 0xff
    end
    match_str = input_str:match('wp=%s*(%a+)%s*;*%s*')
    local valid_policy = {
        ['wt'] = 0,
        ['wbwithbbu'] = 1,
        ['wb'] = 2
    }
    if not match_str or not valid_policy[match_str] then
        error(base_messages.PropertyValueFormatError(input_str, 'wp'))
    end

    return valid_policy[match_str]
end

function m.get_para_io_policy(input_str)
    local match_str = input_str:match('iop=')
    if not match_str then
        return 0xff
    end
    local match_str = input_str:match('iop=%s*(%a+)%s*;*%s*')
    local valid_policy = {
        ['cio'] = 0,
        ['dio'] = 1
    }
    if not match_str or not valid_policy[match_str] then
        error(base_messages.PropertyValueFormatError(input_str, 'iop'))
    end

    return valid_policy[match_str]
end

function m.get_para_access_policy(input_str)
    local match_str = input_str:match('ap=')
    if not match_str then
        return 0xff
    end
    match_str = input_str:match('ap=%s*(%a+)%s*;*%s*')
    local valid_policy = {
        ['rw'] = 0,
        ['ro'] = 1,
        ['blocked'] = 2
    }
    if not match_str or not valid_policy[match_str] then
        error(base_messages.PropertyValueFormatError(input_str, 'ap'))
    end

    return valid_policy[match_str]
end

function m.get_para_disk_cache_policy(input_str)
    local match_str = input_str:match('dcp=')
    if not match_str then
        return 0xff
    end
    match_str = input_str:match('dcp=%s*(%a+)%s*;*%s*')
    local valid_policy = {
        ['default'] = 0,
        ['enabled'] = 1,
        ['disabled'] = 2
    }
    if not match_str or not valid_policy[match_str] then
        error(base_messages.PropertyValueFormatError(input_str, 'dcp'))
    end

    return valid_policy[match_str]
end

function m.get_para_init_type(input_str)
    local match_str = input_str:match('init=')
    if not match_str then
        return 0xff
    end
    match_str = input_str:match('init=%s*(%a+)%s*;*%s*')
    local valid_policy = {
        ['no'] = 0,
        ['quick'] = 1,
        ['full'] = 2,
        ['rpi'] = 3,
        ['opo'] = 4
    }
    if not match_str or not valid_policy[match_str] then
        error(base_messages.PropertyValueFormatError(input_str, 'init'))
    end

    return valid_policy[match_str]
end

function m.get_para_array_id(input_str)
    local match_str = input_str:match('array=%s*(%d+)%s*;*%s*')
    if not match_str then
        error(base_messages.PropertyValueFormatError(input_str, 'array'))
    end
    local array_id = tonumber(match_str)
    if not array_id then
        error(base_messages.PropertyValueFormatError(input_str, 'array'))
    end
    --界面值到业务内部值映射
    return array_id - 1
end

function m.get_para_block_index(input_str)
    local match_str = input_str:match('block=')
    if not match_str then
        return 0xff
    end
    match_str = input_str:match('block=%s*(%d+)%s*;*%s*')
    if not match_str then
        error(base_messages.PropertyValueFormatError(input_str, 'block'))
    end
    local block_index = tonumber(match_str)
    if not block_index or block_index == SML_MAX_HOLES_IN_ARRAY or
        block_index == 0 then
        error(base_messages.PropertyValueFormatError(input_str, 'block'))
    end
    return block_index - 1
end

--控制器：BBU(Battery)相关接口
local function is_battery_exist(bbu_path)
    local ok, rsp = pcall(mdb_service.is_valid_path, bus, bbu_path)
    if not ok then
        log:error("bbu path is invalid: %s, err:%s", bbu_path, rsp.message)
        error(rsp)
    end
    return rsp.Result
end

function m.get_controller_bbu_presence(bbu_path, storage_state)
    local is_exist = is_battery_exist(bbu_path)
    --该controller没有battery, storage_state无效
    if not is_exist then
        return 0xff
    end
    return storage_state == 0xff and 0xff or storage_state + 1
end

function m.get_controller_bbu_type(bbu_path, storage_type, state)
    local is_exist = is_battery_exist(bbu_path)
    if not is_exist or state == 0xff then
        return "N/A"
    end
    return storage_type
end

function m.get_concated_raid_levels_str(raid_levels_array)
    if #raid_levels_array == 0 then
        return "N/A"
    end
    return table.concat(raid_levels_array, ',')
end

--diskArrayProperty相关接口
function m.get_ref_logiacl_ids(ref_logical_ids)
    if #ref_logical_ids == 0 then
        return "unknown"
    end

    local out_str = ""
    for k, v in pairs(ref_logical_ids) do
        if v == 0xffff then
            return "unknown"
        end
        out_str = out_str .. (v + 1) .. ','
    end
    return string.sub(out_str, 1, -2)
end

local function get_pd_id_from_ref_drive(drive_str)
    local match_str = string.match(drive_str, 'Disk(%d+)')
    if not match_str then
        log:error('get_pd_id_from_ref_drive  match failed! source str is %s', drive_str)
        return 0xff
    end
    local drive_index = tonumber(match_str)
    if not drive_index then
        log:error('get_pd_id_from_ref_drive get drive index failed, match_str is %s', match_str)
        return 0xff
    end
    return drive_index
end

function m.get_pd_id_list(ref_drive_ids)
    if #ref_drive_ids == 0 then
        return "unknown"
    end

    local out_str = ""
    for k, v in pairs(ref_drive_ids) do
        local drive_index = get_pd_id_from_ref_drive(v)
        if drive_index == 0xff then
            return "unknown"
        end
        out_str = out_str .. (drive_index + 1) .. ','
    end
    return string.sub(out_str, 1, -2)
end

function m.get_free_block_string(free_block_list)
    if #free_block_list == 0 then
        return "unknown"
    end

    local out_str = ""
    local loop_sum = #free_block_list
    for i = 1, loop_sum do
        if free_block_list[i] == 0xFFFFFFFF then
            out_str = out_str .. string.format("(%u)%u", i, 0) .. ','
        else
            out_str = out_str .. string.format("(%u)%u", i, free_block_list[i]) .. ','
        end
    end
    return string.sub(out_str, 1, -2)
end

return m
